/*
 * Simple Linux HW watchdog daemon
 *
 * Copyright (c) 2010 Daniel Widyanko. All rights reserved.
 * Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <getopt.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>

#include <linux/watchdog.h>

#define WATCHDOGDEV "/dev/watchdog"
#define MIN_WD_TIMEOUT 5	/* WD must be at least 5 seconds */

#define PETTING_SECS 2		/* Try to pet every 2 seconds */

/* "volatile" prevents gcc from optimizing accesses to terminated. */
static volatile sig_atomic_t terminated;
static const char short_options[] = "hcd:i:";
static const struct option long_options[] = {
	{"help", 0, NULL, 'h'},
	{"check", 0, NULL, 'c'},
	{"dev", 1, NULL, 'd'},
	{"interval", 1, NULL, 'i'},
	{NULL, 0, NULL, 0},
};

static void print_usage(FILE * stream, char *app_name, int exit_code)
{
	fprintf(stream, "Usage: %s [options]\n", app_name);
	fprintf(stream,
" -h  --help                Display this usage information.\n"
" -c  --check               Exit right away after printing info.\n"
" -d  --dev <device_file>   Use <device_file> as HW watchdog device file.\n"
"                           The default is '/dev/watchdog'\n"
" -i  --interval <interval> Change the HW watchdog interval time\n"
"                           Must be at least %d seconds\n", MIN_WD_TIMEOUT
	);

	exit(exit_code);
}


static void daisydog_sigterm(int signal)
{
	terminated = 1;
}

int main(int argc, char **argv)
{
	int fd;			/* File handler for HW watchdog */
	int bootstatus;		/* HW Watchdog last boot status */
	char *dev = WATCHDOGDEV;/* HW Watchdog default device file */

	int next_option;	/* getopt iteration var */
	int wd_timeout;		/* when HW watchdog goes balistic */
	int interval = 0;	/* user parameter for wd_timeout */
	ssize_t ret = 0;	/* write/sleep call return value */

	/* Parse options if any */
	do {
		next_option = getopt_long(argc, argv, short_options,
					long_options, NULL);
		switch (next_option) {
		case 'h':
			print_usage(stdout, argv[0], EXIT_SUCCESS);
		case 'c':
			terminated = 1;
			break;
		case 'd':
			dev = optarg;
			break;
		case 'i':
			interval = atoi(optarg);
			if (interval < MIN_WD_TIMEOUT)
				print_usage(stderr, argv[0], -EINVAL);
			break;
		case '?':	/* Invalid options */
			print_usage(stderr, argv[0], -EINVAL);
		case -1:	/* Done with options */
			break;
		default:	/* Unexpected stuffs */
			abort();
		}
	} while (next_option != -1);

	/* Once the watchdog device file is open, the watchdog will
	 * be activated by the driver.
	 */
	fd = open(dev, O_RDWR);
	if (-1 == fd) {
		fprintf(stderr, "Error: %m\n"); /* errno */
		exit(EXIT_FAILURE);
	}

	signal(SIGTERM, daisydog_sigterm);
	signal(SIGHUP, daisydog_sigterm);
	signal(SIGINT, daisydog_sigterm);

	/* If user wants to change the HW watchdog timeout */
	if (interval) {
		if (ioctl(fd, WDIOC_SETTIMEOUT, &interval) != 0) {
			fprintf(stderr, "Error: Could not set HW watchdog"
					"interval to %d\n", interval);
			exit(EXIT_FAILURE);
		}

	}

	/* Get/Display current HW watchdog interval.
	 * Let user know if it's not exactly what they specified.
	 */
	if (ioctl(fd, WDIOC_GETTIMEOUT, &wd_timeout) == 0) {
		fprintf(stdout, "HW watchdog interval is %d seconds",
				wd_timeout);

		if (interval && interval != wd_timeout)
			fprintf(stdout, " (user asked for %d seconds)\n",
					interval);

		fprintf(stdout, "\n");
	} else {
		fprintf(stderr, "Error: Cannot read HW watchdog interval\n");
		exit(EXIT_FAILURE);
	}

	/* Check if last boot is caused by HW watchdog */
	if (ioctl(fd, WDIOC_GETBOOTSTATUS, &bootstatus) == 0) {

		printf("%s reported boot status: ", dev);

		if (bootstatus == 0)
			puts("normal-boot");
		else if (bootstatus == -1)
			puts("UNKNOWN");
		else {
			/* show hex value in case unknown bits are set */
			printf("%#0x", bootstatus);

			if (bootstatus & WDIOF_CARDRESET)
				printf(" watchdog-timeout");
			if (bootstatus & WDIOF_OVERHEAT)
				printf(" CPU-overheat");
			if (bootstatus & WDIOF_POWERUNDER)
				printf(" power-undervoltage");
			if (bootstatus & WDIOF_POWEROVER)
				printf(" power-overvoltage");
			if (bootstatus & WDIOF_FANFAULT)
				printf(" fan-fault");

			putchar('\n');
		}

	} else {
		fprintf(stderr, "ERROR: Cannot read %s boot status\n", dev);
		exit(EXIT_FAILURE);
	}

	/* There are two ways to pet the watchdog:
	 * 1) by writing any dummy value into watchdog device file, or
	 * 2) by using IOCTL WDIOC_KEEPALIVE
	 *
	 * Use the first method since it's easier. :)
	 */
	while (!terminated) {
		ret = write(fd, "w", 1);

		/* force immediate exit of loop if write fails. */
		if (ret != 1) {
			fprintf(stderr, "Terminating (%m)\n"); /* errno */
			ret = errno;
			break;
		}

		sleep(PETTING_SECS);

		/* SIGTERM/HUP/INT will cause sleep(3) to return early.
		 * SIGKILL will exit anyway.
		 * If something else caused us to return early, just
		 * pretend it was a hiccup and keep looping.
		 */
	}

	/* Writing 'V' into watchdog device indicates the close/stop
	 * of the watchdog was intentional. Otherwise, debug message
	 * 'Watchdog timer closed unexpectedly' will be printed to
	 * dmesg and the system will reboot in wd_timeout seconds since
	 * the last time the watchdog was pet.
	 */
	if (write(fd, "V", 1))
		/* shut gcc up */;

	/* Closing the watchdog device will deactivate the watchdog. */
	close(fd);

	exit(ret);
}
